Embedding
Go does not provide the typical, type-driven notion of subclassing, but it does have the ability to “borrow” pieces of an implementation by_embedding_types within a struct or interface.
Interface embedding is very simple. We’ve mentioned theio.Reader
andio.Writer
interfaces before; here are their definitions.
1 | type Reader interface { |
Theio
package also exports several other interfaces that specify objects that can implement several such methods. For instance, there isio.ReadWriter
, an interface containing bothRead
andWrite
. We could specifyio.ReadWriter
by listing the two methods explicitly, but it’s easier and more evocative to embed the two interfaces to form the new one, like this:
1 | // ReadWriter is the interface that combines the Reader and Writer interfaces. |
This says just what it looks like: AReadWriter
can do what aReader
does_and_what aWriter
does; it is a union of the embedded interfaces (which must be disjoint sets of methods). Only interfaces can be embedded within interfaces.
The same basic idea applies to structs, but with more far-reaching implications. Thebufio
package has two struct types,bufio.Reader
andbufio.Writer
, each of which of course implements the analogous interfaces from packageio
. Andbufio
also implements a buffered reader/writer, which it does by combining a reader and a writer into one struct using embedding: it lists the types within the struct but does not give them field names.
1 | // ReadWriter stores pointers to a Reader and a Writer. |
The embedded elements are pointers to structs and of course must be initialized to point to valid structs before they can be used. TheReadWriter
struct could be written as
1 | type ReadWriter struct { |
but then to promote the methods of the fields and to satisfy theio
interfaces, we would also need to provide forwarding methods, like this:
1 | func (rw *ReadWriter) Read(p []byte) (n int, err error) { |
By embedding the structs directly, we avoid this bookkeeping. The methods of embedded types come along for free, which means thatbufio.ReadWriter
not only has the methods ofbufio.Reader
andbufio.Writer
, it also satisfies all three interfaces:io.Reader
,io.Writer
, andio.ReadWriter
.
There’s an important way in which embedding differs from subclassing. When we embed a type, the methods of that type become methods of the outer type, but when they are invoked the receiver of the method is the inner type, not the outer one. In our example, when theRead
method of abufio.ReadWriter
is invoked, it has exactly the same effect as the forwarding method written out above; the receiver is thereader
field of theReadWriter
, not theReadWriter
itself.
Embedding can also be a simple convenience. This example shows an embedded field alongside a regular, named field.
1 | type Job struct { |
TheJob
type now has theLog
,Logf
and other methods of*log.Logger
. We could have given theLogger
a field name, of course, but it’s not necessary to do so. And now, once initialized, we can log to theJob
:
1 | job.Log("starting now...") |
TheLogger
is a regular field of theJob
struct, so we can initialize it in the usual way inside the constructor forJob
, like this,
1 | func NewJob(command string, logger *log.Logger) *Job { |
or with a composite literal,
1 | job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)} |
If we need to refer to an embedded field directly, the type name of the field, ignoring the package qualifier, serves as a field name, as it did in theRead
method of ourReadWriter
struct. Here, if we needed to access the*log.Logger
of aJob
variablejob
, we would writejob.Logger
, which would be useful if we wanted to refine the methods ofLogger
.
1 | func (job *Job) Logf(format string, args ...interface{}) { |
Embedding types introduces the problem of name conflicts but the rules to resolve them are simple. First, a field or methodX
hides any other itemX
in a more deeply nested part of the type. Iflog.Logger
contained a field or method calledCommand
, theCommand
field ofJob
would dominate it.
Second, if the same name appears at the same nesting level, it is usually an error; it would be erroneous to embedlog.Logger
if theJob
struct contained another field or method calledLogger
. However, if the duplicate name is never mentioned in the program outside the type definition, it is OK. This qualification provides some protection against changes made to types embedded from outside; there is no problem if a field is added that conflicts with another field in another subtype if neither field is ever used.